// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.

package contact

import (
	"context"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/spacemonkeygo/monkit/v3"
	"github.com/zeebo/errs"
	"go.uber.org/zap"
	"golang.org/x/sync/errgroup"

	"storj.io/common/pb"
	"storj.io/common/rpc"
	"storj.io/common/storj"
	"storj.io/common/sync2"
	"storj.io/storj/storagenode/trust"
)

var (
	mon = monkit.Package()

	// Error is the default error class for contact package.
	Error = errs.Class("contact")

	errPingSatellite = errs.Class("ping satellite")
)

const initialBackOff = time.Second

// Config contains configurable values for contact service.
type Config struct {
	ExternalAddress string `user:"true" help:"the public address of the node, useful for nodes behind NAT" default:""`

	// Chore config values
	Interval time.Duration `help:"how frequently the node contact chore should run" releaseDefault:"30s" devDefault:"30s"`
}

// NodeInfo contains information necessary for introducing storagenode to satellite.
type NodeInfo struct {
	ID       storj.NodeID
	Address  string
	P2PAddress string
	Version  pb.NodeVersion
	Capacity pb.NodeCapacity
	Operator pb.NodeOperator
}

// Service is the contact service between storage nodes and satellites.
type Service struct {
	log    *zap.Logger
	dialer rpc.Dialer
	mu   sync.Mutex
	self NodeInfo
	trust *trust.Pool
	initialized sync2.Fence
	lastContact time.Time
	heartbeatContact string
}

// NewService creates a new contact service.
func NewService(log *zap.Logger, dialer rpc.Dialer, self NodeInfo, trust *trust.Pool) *Service {
	return &Service{
		log:    log,
		dialer: dialer,
		trust:  trust,
		self:   self,
	}
}

// PingSatellites attempts to ping all satellites in trusted list until backoff reaches maxInterval.
func (service *Service) PingSatellites(ctx context.Context, maxInterval time.Duration) (err error) {
	defer mon.Task()(&ctx)(&err)
	satellites := service.trust.GetSatellites(ctx)
	var group errgroup.Group
	for _, satellite := range satellites {
		satellite := satellite
		group.Go(func() error {
			_, err :=  service.pingSatellite(ctx, satellite, maxInterval)
			return err
		})
	}
	return group.Wait()
}


func (service *Service) pingSatellite(ctx context.Context, satellite storj.NodeID, maxInterval time.Duration) (int ,error ){
	mon.Meter("satellite_contact_request").Mark(1) //mon:locked
	interval, err := service.pingSatelliteOnce(ctx, satellite)
	if err != nil{
		service.log.Error("ping satellite failed ", zap.Stringer("Satellite ID", satellite), zap.Error(err))
	}
	//原代码的这个函数总是返回nil。如果返回err，上层函数会结束循环
	return interval, nil
}

func (service *Service) pingSatelliteOnce(ctx context.Context, id storj.NodeID) (interVal int, err error) {
	defer mon.Task()(&ctx, id)(&err)

	nodeurl, err := service.trust.GetNodeURL(ctx, id)
	if err != nil {
		return 0,errPingSatellite.Wrap(err)
	}
	needSendData := false
	if  (time.Now().Sub(service.lastContact)) > time.Minute*30{
		needSendData = true
		service.lastContact = time.Now()
	}
	if needSendData{ //原默认发数据时长为1小时
		service.log.Info("pingSatelliteOnce with data")
		conn, err := service.dialer.DialNodeURL(ctx, nodeurl)
		if err != nil {
			return 0, errPingSatellite.Wrap(err)
		}
		defer func() { err = errs.Combine(err, conn.Close()) }()
		self := service.Local()
		resp, err := pb.NewDRPCNodeClient(conn).CheckIn(ctx,&pb.CheckInRequest{
			Address:  self.Address,
			Version:  &self.Version,
			Capacity: &self.Capacity,
			Operator: &self.Operator,
			P2PAddress: self.P2PAddress,
		})
		if err != nil {
			return 0,errPingSatellite.Wrap(err)
		}
		service.log.Info("resp UserMessage ",zap.String("",resp.UserMessage))
		arr := strings.Split(resp.UserMessage,"#")
		service.heartbeatContact = arr[0]
		if len(arr) > 1{
			interVal , err = strconv.Atoi(arr[1])
		}

		if resp != nil && !resp.PingNodeSuccess {
			return 0,errPingSatellite.New("%s", resp.PingErrorMessage)
		}
		if resp.PingErrorMessage != "" {
			service.log.Warn("", zap.Stringer("Satellite ID", id), zap.String("Error", resp.GetPingErrorMessage()))
		}
	}else{
		service.log.Info("pingSatelliteOnce hearbeat")
		var nodeUrls []storj.NodeURL
		ipPorts := strings.Split(service.heartbeatContact,",")
		for _,ipPort := range ipPorts{
			nodeUrls = append(nodeUrls,storj.NodeURL{//core
				ID:         nodeurl.ID,
				Address:    ipPort,
			})
		}
		for _, url := range nodeUrls{
			//go func(url storj.NodeURL) {
			ctx2, cancel := context.WithTimeout(ctx,time.Second*3)
			defer cancel()
			conn, err := service.dialer.DialNodeURL(ctx2, url)
			if err != nil {
				service.log.Error("pingSatellite err ", zap.Error(err))
				return interVal,err
			}
			defer func() { err = errs.Combine(err, conn.Close()) }()
			resp, err := pb.NewDRPCNodeClient(conn).CheckIn(ctx2, &pb.CheckInRequest{})
			if err != nil {
				return interVal,err
			}
			if resp != nil && !resp.PingNodeSuccess {
				service.log.Error("pingSatellite resp != nil ")
				return interVal,err
			}
		}
	}
	return interVal,nil
}

// Local returns the storagenode info.
func (service *Service) Local() NodeInfo {
	service.mu.Lock()
	defer service.mu.Unlock()
	return service.self
}

// UpdateSelf updates the local node with the capacity.
func (service *Service) UpdateSelf(capacity *pb.NodeCapacity) {
	service.mu.Lock()
	defer service.mu.Unlock()
	if capacity != nil {
		service.self.Capacity = *capacity
	}
	service.initialized.Release()
}
func  (service *Service)  PingGateway(address string, tryCount int) (err error) {
	ctx := context.TODO()
	var cancelFun func()
	ctx , cancelFun = context.WithTimeout(ctx, time.Second*6)
	defer cancelFun()

	for i := 0; i < tryCount; i++{
		conn, err := service.dialer.DialAddressInsecure(ctx, address)

		if err != nil {
			service.log.Error("PingGateway  dail error", zap.Error(err))
			return errPingSatellite.Wrap(err)
		}
		defer func() { err = errs.Combine(err, conn.Close()) }()

		_, err = pb.NewDRPCNodeClient(conn).CheckIn(ctx, &pb.CheckInRequest{
		})

		if err != nil {
			service.log.Error("ping gateway error", zap.Error(err))
			//return errPingSatellite.Wrap(err)
		}else{
			break
		}

		time.Sleep(time.Second*2)
	}
	return nil

}